/*
 * Copyright (c) 2009 XMock contributors
 * This program is made available under the terms of the Apache License.
 */
package org.xmock.aspectj;

import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.JexlHelper;
import org.aspectj.lang.reflect.MethodSignature;


import org.apache.log4j.Logger;
import org.xmock.annotation.MockCase;
import org.xmock.annotation.MockRecord;
import org.xmock.controller.MockCaseManager;
import org.xmock.controller.MockController;
import org.xmock.controller.MockFactory;
import org.xmock.controller.MockObjectManager;
import org.xmock.utility.XStreamUtility;

import java.util.StringTokenizer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

aspect MockAspect {

	private static final Logger logger = Logger.getLogger(MockAspect.class);
	
	pointcut mockCaseMethod (MockCase mockCase) : 
		!within (MockAspect) && 
		execution (@MockCase * *(..)) && 
		@annotation (mockCase);
	
	Object around (MockCase mockCase) throws Exception : mockCaseMethod (mockCase) 
	{
		Object returnObject = null;

		Object[]    argValues = thisJoinPoint.getArgs();
		Object object = thisJoinPoint.getTarget();
	    JexlContext jc = JexlHelper.createContext();
	    jc.getVars().put("$args", argValues );
	    jc.getVars().put("$this",object);
	    
	    String mockCaseName = expressionCheck(mockCase.name(), jc);		
		String mockCaseId = expressionCheck(mockCase.id(), jc);
		logger.debug("MockCase name:" + mockCaseName + "  value:" + mockCaseId);
		
		org.xmock.controller.MockCase mockCaseInfo = null;
		MockCaseManager caseManager = null;
		MockObjectManager objectManager = null;
		try
		{
			caseManager = MockFactory.getInstance().createMockCaseManager();
			objectManager = MockFactory.getInstance().createMockObjectManager();		
			mockCaseInfo = caseManager.getMockCase(mockCaseName,mockCaseId);
		}
		catch(Exception exception)
		{
			logger.error("Error configuring xmock framework", exception);
		}
		
		if(mockCaseInfo != null)
		{
			logger.debug("Mock Case Match Found");

		    jc.getVars().put("$case",mockCaseInfo);
			
			MethodSignature ms = (MethodSignature) thisJoinPoint.getSignature();
			String methodSignature = ms.toString();
			boolean mockObjectPresent = objectManager.isMockObjectPresent(methodSignature, mockCaseName, mockCaseId);
			
			if(mockCaseInfo.isRerecord() || (MockController.getInstance().isMockModeRecord() && !mockObjectPresent))
			{
				logger.debug("Mock Mode Record");
				try
				{
					returnObject = proceed(mockCase);
					objectManager.saveMockObject(methodSignature, mockCaseName, mockCaseId, returnObject);
				}
				catch(Exception exception)
				{
					returnObject = exception;
					objectManager.saveMockObject(methodSignature, mockCaseName, mockCaseId, returnObject);
					throw exception;
				}

				Annotation[] annotationArray = ms.getMethod().getDeclaredAnnotations();
				for(int i=0; i<annotationArray.length; i++)
				{
					if(annotationArray[i] instanceof MockRecord)
					{
						MockRecord mockRecord = (MockRecord) annotationArray[i];
						String recordExpression = mockRecord.source();
						Object recordObject = jexlEvaluate(recordExpression, jc);
						objectManager.saveMockObject(methodSignature, mockCaseName, mockCaseId, mockRecord.name(), recordObject);
					}
				}
			}			
			else if(mockObjectPresent && MockController.getInstance().isMockModePlay())
			{
				logger.debug("Mock Mode Play");	
				returnObject = objectManager.getMockObject(methodSignature, mockCaseName, mockCaseId);
				if(returnObject instanceof Exception)
				{
					throw (Exception)returnObject;
				}
				
				Annotation[] annotationArray = ms.getMethod().getDeclaredAnnotations();
				for(int i=0; i<annotationArray.length; i++)
				{
					if(annotationArray[i] instanceof MockRecord)
					{
						MockRecord mockRecord = (MockRecord) annotationArray[i];
						Object recordObject = objectManager.getMockObject(methodSignature, mockCaseName, mockCaseId, mockRecord.name());
						jc.getVars().put("$" + mockRecord.name(),recordObject);
						String recordExpression = mockRecord.target();
						if(recordExpression == null || recordExpression.length()==0)
						{
							try
							{
								setTargetObject(recordExpression, jc, recordObject);
							}
							catch(Exception ex)
							{
								logger.error("Error setting target Object", ex);
							}
						}
					}
				}
			}
			else
			{
				logger.debug("Mock Case Match but not mocking");
				returnObject = proceed(mockCase);
			}
		}
		else
		{
			logger.debug("Mock Case Match Not Found");
			returnObject = proceed(mockCase);
		}
		return returnObject;
	}
	
	private String expressionCheck(String expression, JexlContext jc)
	{
		String returnValue = "";
		if(expression.indexOf("+")>=0)
		{
			StringTokenizer token = new StringTokenizer(expression, "+");
			while(token.hasMoreElements())
			{
				returnValue = returnValue + jexlEvaluate(token.nextToken(), jc);
			}
		}
		else
		{
			returnValue = returnValue + jexlEvaluate(expression, jc);
		}
		return returnValue;
	}
	
	private Object jexlEvaluate(String expression, JexlContext jc)
	{
		logger.debug("Expression" + expression);
		Object returnValue = null;
		if(expression.indexOf("$")==0)
		{
			try {
				Expression e = ExpressionFactory.createExpression(expression);
				returnValue = e.evaluate(jc);
			} 
			catch (Exception e) 
			{
				logger.error(e);
			}
		}
		else
		{
			returnValue = expression;
		}
		return returnValue;
	}
	
	private void setTargetObject(String expression, JexlContext jc, Object obj) throws Exception
	{
		String targetObjectExpression = expression.substring(0,expression.lastIndexOf("."));
		Object targetObject = jexlEvaluate(targetObjectExpression, jc);
		XStreamUtility.consoleLogObject(obj);
		String methodName = expression.substring(expression.lastIndexOf(".")+1);
		Method method = targetObject.getClass().getMethod(methodName, obj.getClass());
		method.invoke(targetObject, obj);
	}
	
}
